在真实的数据分析项目中,数据往往分散在多个来源中:
将不同来源的数据整合在一起,是日常数据分析的核心挑战。
| 问题 | 说明 |
|---|---|
| 粒度不匹配 | 日频行情 vs 季频财务 |
| 时间对齐 | 不同市场的交易日历不同 |
| 键值识别 | 如何正确匹配同一公司的不同数据源? |
| 重复数据 | 同一指标来自多个提供商 |
| 性能瓶颈 | 大规模数据集的合并操作极其耗时 |
concat、merge、join 的本质区别与应用场景设两个数据矩阵 \(D_1\)(\(n\) 行)和 \(D_2\)(\(m\) 行),垂直拼接结果为:
\[ D_{\text{concat}} = \begin{bmatrix} D_1 \\ D_2 \end{bmatrix} \]
前提条件:两个数据集必须具有相同的列结构(相同列名和数据类型)。
将多个数据集沿列方向(横向)合并,增加变量数量:
\[ D_{\text{merge}} = [D_1 \mid D_2] \]
前提条件:两个数据集必须具有相同的行数或可通过键值对齐。
Pandas 的 merge 操作基于关系代数中的连接(Join)运算:
\[ R \bowtie_{\theta} S = \{ (r, s) \in R \times S \mid \theta(r, s) \} \]
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
price_JantoMar = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)#从外部导入Sheet1的5只股票信息链接为https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx
print(price_JantoMar.head()) #查看前五行数据
print(price_JantoMar.tail()) #查看后五行数据
price_AprtoJui = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)##从外部导入Sheet2的5只股票信息链接为https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx
print(price_AprtoJui.head()) #查看前五行数据
print(price_AprtoJui.tail()) #查看后五行数据 中国移动 中国电信 中国人寿
日期
2019-01-02 47.51 50.60 10.44
2019-01-03 47.39 49.53 10.09
2019-01-04 49.23 50.44 10.55
2019-01-07 49.91 51.08 10.59
2019-01-08 50.33 51.36 10.72
中国移动 中国电信 中国人寿
日期
2019-07-25 43.41 46.17 13.08
2019-07-26 43.48 45.58 13.08
2019-07-29 43.29 45.39 13.00
2019-07-30 42.85 45.00 12.86
2019-07-31 42.60 44.74 12.74
中国铝业 中国海洋石油
日期
2019-01-02 7.88 150.00
2019-01-03 7.63 147.59
2019-01-04 7.98 155.74
2019-01-07 8.15 157.99
2019-01-08 8.48 161.07
中国铝业 中国海洋石油
日期
2019-07-25 8.21 167.80
2019-07-26 8.29 166.82
2019-07-29 8.30 167.54
2019-07-30 8.21 167.00
2019-07-31 8.05 165.33
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
# 从Excel文件读取数据存入price_JantoMar
price_JantoMar = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)
# 从Excel文件读取数据存入price_AprtoJui
price_AprtoJui = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)
price_JantoJul = pd.concat([price_JantoMar,price_AprtoJui],axis=0) #使用concat函数按行拼接
print(price_JantoJul.head()) #前五行数据
print(price_JantoJul.tail()) #后五行数据 中国移动 中国电信 中国人寿 中国铝业 中国海洋石油
日期
2019-01-02 47.51 50.60 10.44 NaN NaN
2019-01-03 47.39 49.53 10.09 NaN NaN
2019-01-04 49.23 50.44 10.55 NaN NaN
2019-01-07 49.91 51.08 10.59 NaN NaN
2019-01-08 50.33 51.36 10.72 NaN NaN
中国移动 中国电信 中国人寿 中国铝业 中国海洋石油
日期
2019-07-25 NaN NaN NaN 8.21 167.80
2019-07-26 NaN NaN NaN 8.29 166.82
2019-07-29 NaN NaN NaN 8.30 167.54
2019-07-30 NaN NaN NaN 8.21 167.00
2019-07-31 NaN NaN NaN 8.05 165.33
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
price_3stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据Sheet1 链接https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx
print(price_3stocks.head()) #查看前五行数据
print(price_3stocks.tail()) #查看后五行数据
price_2stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0) #导入数据Sheet1 链接https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx
print(price_2stocks.head()) #查看前五行数据
print(price_2stocks.tail()) #查看后五行数据 中国移动 中国电信 中国人寿
日期
2019-01-02 47.51 50.60 10.44
2019-01-03 47.39 49.53 10.09
2019-01-04 49.23 50.44 10.55
2019-01-07 49.91 51.08 10.59
2019-01-08 50.33 51.36 10.72
中国移动 中国电信 中国人寿
日期
2019-07-25 43.41 46.17 13.08
2019-07-26 43.48 45.58 13.08
2019-07-29 43.29 45.39 13.00
2019-07-30 42.85 45.00 12.86
2019-07-31 42.60 44.74 12.74
中国铝业 中国海洋石油
日期
2019-01-02 7.88 150.00
2019-01-03 7.63 147.59
2019-01-04 7.98 155.74
2019-01-07 8.15 157.99
2019-01-08 8.48 161.07
中国铝业 中国海洋石油
日期
2019-07-25 8.21 167.80
2019-07-26 8.29 166.82
2019-07-29 8.30 167.54
2019-07-30 8.21 167.00
2019-07-31 8.05 165.33
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
# 从Excel文件读取数据存入price_3stocks
price_3stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)
# 从Excel文件读取数据存入price_2stocks
price_2stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)
price_5stocks_concat = pd.concat([price_3stocks,price_2stocks],axis=1) #使用concat函数按列拼接
print(price_5stocks_concat.head()) #查看前五行数据
price_5stocks_merge = pd.merge(left=price_3stocks,right=price_2stocks,left_index=True,right_index=True) #使用merge函数按列拼接
print(price_5stocks_merge.head()) #查看前五行数据
price_5stocks_join = price_3stocks.join(price_2stocks,on="日期") #用join函数按列拼接
print(price_5stocks_join.head()) #查看前五行数据 中国移动 中国电信 中国人寿 中国铝业 中国海洋石油
日期
2019-01-02 47.51 50.60 10.44 7.88 150.00
2019-01-03 47.39 49.53 10.09 7.63 147.59
2019-01-04 49.23 50.44 10.55 7.98 155.74
2019-01-07 49.91 51.08 10.59 8.15 157.99
2019-01-08 50.33 51.36 10.72 8.48 161.07
中国移动 中国电信 中国人寿 中国铝业 中国海洋石油
日期
2019-01-02 47.51 50.60 10.44 7.88 150.00
2019-01-03 47.39 49.53 10.09 7.63 147.59
2019-01-04 49.23 50.44 10.55 7.98 155.74
2019-01-07 49.91 51.08 10.59 8.15 157.99
2019-01-08 50.33 51.36 10.72 8.48 161.07
中国移动 中国电信 中国人寿 中国铝业 中国海洋石油
日期
2019-01-02 47.51 50.60 10.44 7.88 150.00
2019-01-03 47.39 49.53 10.09 7.63 147.59
2019-01-04 49.23 50.44 10.55 7.98 155.74
2019-01-07 49.91 51.08 10.59 8.15 157.99
2019-01-08 50.33 51.36 10.72 8.48 161.07
pd.concat() 是 Pandas 中最通用的拼接函数,核心语法:
axis=0:垂直拼接(行方向堆叠)axis=1:水平拼接(列方向合并)ignore_index=True:重置索引,避免重复| 参数 | 作用 | 默认值 |
|---|---|---|
objs |
要拼接的对象列表 | 必需 |
axis |
拼接方向(0=行,1=列) | 0 |
ignore_index |
忽略原索引,重新生成 | False |
keys |
创建多级索引标识来源 | None |
join |
列对齐方式(inner/outer) | outer |
sort |
是否对列排序 | True |
import pandas as pd
import numpy as np
# 贵州茅台(600519.SH)收益率数据
stock_a_returns = pd.DataFrame({
'日期': pd.date_range('2024-01-01', periods=3),
'股票代码': ['600519.SH'] * 3,
'收益率': [0.02, -0.01, 0.03]
})
# 五粮液(000858.SZ)收益率数据
stock_b_returns = pd.DataFrame({
'日期': pd.date_range('2024-01-04', periods=3),
'股票代码': ['000858.SZ'] * 3,
'收益率': [0.01, 0.02, -0.02]
})
# 垂直拼接,ignore_index=True 重置索引
all_returns = pd.concat(
[stock_a_returns, stock_b_returns], ignore_index=True
)
print('股票A数据:')
print(stock_a_returns)
print('\n股票B数据:')
print(stock_b_returns)
print('\n拼接结果:')
print(all_returns)股票A数据:
日期 股票代码 收益率
0 2024-01-01 600519.SH 0.02
1 2024-01-02 600519.SH -0.01
2 2024-01-03 600519.SH 0.03
股票B数据:
日期 股票代码 收益率
0 2024-01-04 000858.SZ 0.01
1 2024-01-05 000858.SZ 0.02
2 2024-01-06 000858.SZ -0.02
拼接结果:
日期 股票代码 收益率
0 2024-01-01 600519.SH 0.02
1 2024-01-02 600519.SH -0.01
2 2024-01-03 600519.SH 0.03
3 2024-01-04 000858.SZ 0.01
4 2024-01-05 000858.SZ 0.02
5 2024-01-06 000858.SZ -0.02
# keys 参数为每个数据框分配标识键,创建多级索引
multi_index_returns = pd.concat(
[stock_a_returns, stock_b_returns],
keys=['股票A', '股票B'],
names=['数据源', '行号']
)
print('多级索引结构:')
print(multi_index_returns)
print('\n索引信息:')
print(multi_index_returns.index)
# 选择特定来源的数据
print('\n仅选择股票A的数据:')
print(multi_index_returns.loc['股票A'])多级索引结构:
日期 股票代码 收益率
数据源 行号
股票A 0 2024-01-01 600519.SH 0.02
1 2024-01-02 600519.SH -0.01
2 2024-01-03 600519.SH 0.03
股票B 0 2024-01-04 000858.SZ 0.01
1 2024-01-05 000858.SZ 0.02
2 2024-01-06 000858.SZ -0.02
索引信息:
MultiIndex([('股票A', 0),
('股票A', 1),
('股票A', 2),
('股票B', 0),
('股票B', 1),
('股票B', 2)],
names=['数据源', '行号'])
仅选择股票A的数据:
日期 股票代码 收益率
行号
0 2024-01-01 600519.SH 0.02
1 2024-01-02 600519.SH -0.01
2 2024-01-03 600519.SH 0.03
应用场景:多源数据质量检查时,可通过多级索引快速定位问题数据的来源。
pd.merge() 基于共同的键值列进行水平合并:
on:指定匹配的键值列how:连接方式(inner / left / right / outer)# 股票基本信息
stock_info = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '600036.SH'],
'股票名称': ['贵州茅台', '五粮液', '招商银行'],
'行业': ['食品饮料', '食品饮料', '金融']
})
# 股票财务数据(注意:第3只是中国平安,不是招商银行)
financial_data = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '601318.SH'],
'PE': [35.2, 25.8, 10.5],
'PB': [12.5, 8.3, 1.2]
})
# 内连接:只保留两边都有的股票
inner_result = pd.merge(
stock_info, financial_data, on='股票代码', how='inner'
)
print('股票基本信息:')
print(stock_info)
print('\n财务数据:')
print(financial_data)
print('\n内连接结果(只保留两边都有的股票):')
print(inner_result)股票基本信息:
股票代码 股票名称 行业
0 600519.SH 贵州茅台 食品饮料
1 000858.SZ 五粮液 食品饮料
2 600036.SH 招商银行 金融
财务数据:
股票代码 PE PB
0 600519.SH 35.2 12.5
1 000858.SZ 25.8 8.3
2 601318.SH 10.5 1.2
内连接结果(只保留两边都有的股票):
股票代码 股票名称 行业 PE PB
0 600519.SH 贵州茅台 食品饮料 35.2 12.5
1 000858.SZ 五粮液 食品饮料 25.8 8.3
\[ R \bowtie S = \{ (r, s) \mid r[\text{key}] = s[\text{key}] \} \]
只有键值在两个数据集中都存在的行才会被保留。
# 左连接:保留左表所有行
left_result = pd.merge(
stock_info, financial_data, on='股票代码', how='left'
)
# 右连接:保留右表所有行
right_result = pd.merge(
stock_info, financial_data, on='股票代码', how='right'
)
# 外连接:保留所有行
outer_result = pd.merge(
stock_info, financial_data, on='股票代码', how='outer'
)
print('左连接结果(保留左边所有股票):')
print(left_result)
print('\n右连接结果(保留右边所有股票):')
print(right_result)
print('\n外连接结果(保留所有股票):')
print(outer_result)左连接结果(保留左边所有股票):
股票代码 股票名称 行业 PE PB
0 600519.SH 贵州茅台 食品饮料 35.2 12.5
1 000858.SZ 五粮液 食品饮料 25.8 8.3
2 600036.SH 招商银行 金融 NaN NaN
右连接结果(保留右边所有股票):
股票代码 股票名称 行业 PE PB
0 600519.SH 贵州茅台 食品饮料 35.2 12.5
1 000858.SZ 五粮液 食品饮料 25.8 8.3
2 601318.SH NaN NaN 10.5 1.2
外连接结果(保留所有股票):
股票代码 股票名称 行业 PE PB
0 000858.SZ 五粮液 食品饮料 25.8 8.3
1 600036.SH 招商银行 金融 NaN NaN
2 600519.SH 贵州茅台 食品饮料 35.2 12.5
3 601318.SH NaN NaN 10.5 1.2
| 场景 | 推荐类型 | 理由 |
|---|---|---|
| 主数据表匹配补充信息 | left |
保证主表数据不丢失 |
| 数据源可靠性相同 | inner |
只保留两边都有的高质量数据 |
| 整合多个不完整来源 | outer |
最大化信息利用 |
df.join() 是 merge 的特例,专门用于基于索引的合并:
# 收益率数据,股票代码为索引
returns = pd.DataFrame({
'日收益率': [0.02, 0.01, -0.01]
}, index=['600519.SH', '000858.SZ', '600036.SH'])
# 波动率数据,股票代码为索引(第3只不同)
volatility = pd.DataFrame({
'年化波动率': [0.25, 0.30, 0.20]
}, index=['600519.SH', '000858.SZ', '601318.SH'])
# 基于索引左连接
joined_data = returns.join(volatility, how='left')
print('收益率数据:')
print(returns)
print('\n波动率数据:')
print(volatility)
print('\n基于索引的左连接结果:')
print(joined_data)收益率数据:
日收益率
600519.SH 0.02
000858.SZ 0.01
600036.SH -0.01
波动率数据:
年化波动率
600519.SH 0.25
000858.SZ 0.30
601318.SH 0.20
基于索引的左连接结果:
日收益率 年化波动率
600519.SH 0.02 0.25
000858.SZ 0.01 0.30
600036.SH -0.01 NaN
| 方法 | 适用场景 | 拼接方向 | 匹配方式 |
|---|---|---|---|
pd.concat() |
结构相同的数据堆叠 | 纵向/横向 | 索引对齐 |
pd.merge() |
基于键值列的关联 | 横向 | 列值匹配 |
df.join() |
基于索引的关联 | 横向 | 索引匹配 |
任务:整合股票基本信息、行情数据、财务指标,构建完整的分析数据集。
import pandas as pd
# 数据源1:股票基本信息(主表)
stock_basic = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH'],
'股票名称': ['贵州茅台', '五粮液', '招商银行', '中国平安'],
'上市日期': ['2001-08-27', '1998-04-27', '2002-04-09', '2007-03-01'],
'行业': ['食品饮料', '食品饮料', '金融', '金融']
})
# 数据源2:日行情数据(缺少中国平安)
daily_quote = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '600036.SH'],
'收盘价': [1850.00, 158.50, 32.80],
'涨跌幅': [1.5, -0.8, 0.5]
})
# 数据源3:财务指标(缺少招商银行)
financial_metrics = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '601318.SH'],
'ROE': [25.8, 22.3, 15.6],
'负债率': [18.5, 30.2, 92.5]
})# 步骤1:基本信息 + 行情数据(左连接)
step1 = pd.merge(
stock_basic, daily_quote,
on='股票代码', how='left', indicator=True
)
print('步骤1:基本信息 + 行情数据')
print(step1)
# 步骤2:继续加入财务指标
final_data = pd.merge(
step1, financial_metrics,
on='股票代码', how='left', suffixes=('', '_财务')
)
print('\n最终整合结果:')
print(final_data)
# 数据完整性分析
print('\n缺失值统计:')
print(final_data.isna().sum())步骤1:基本信息 + 行情数据
股票代码 股票名称 上市日期 行业 收盘价 涨跌幅 _merge
0 600519.SH 贵州茅台 2001-08-27 食品饮料 1850.0 1.5 both
1 000858.SZ 五粮液 1998-04-27 食品饮料 158.5 -0.8 both
2 600036.SH 招商银行 2002-04-09 金融 32.8 0.5 both
3 601318.SH 中国平安 2007-03-01 金融 NaN NaN left_only
最终整合结果:
股票代码 股票名称 上市日期 行业 收盘价 涨跌幅 _merge ROE 负债率
0 600519.SH 贵州茅台 2001-08-27 食品饮料 1850.0 1.5 both 25.8 18.5
1 000858.SZ 五粮液 1998-04-27 食品饮料 158.5 -0.8 both 22.3 30.2
2 600036.SH 招商银行 2002-04-09 金融 32.8 0.5 both NaN NaN
3 601318.SH 中国平安 2007-03-01 金融 NaN NaN left_only 15.6 92.5
缺失值统计:
股票代码 0
股票名称 0
上市日期 0
行业 0
收盘价 1
涨跌幅 1
_merge 0
ROE 1
负债率 1
dtype: int64
left join 确保不丢失股票indicator=True 标识每条数据是否成功匹配suffixes 参数处理重名列import pandas as pd
import numpy as np
import time
n_stocks = 5000
n_dates = 1000
# 生成大规模测试数据(500万行行情 + 5000行财务)
quotes = pd.DataFrame({
'股票代码': np.repeat(
[f'{i:06d}.SH' for i in range(n_stocks)], n_dates
),
'日期': list(pd.date_range('2020-01-01', periods=n_dates)) * n_stocks,
'收盘价': np.random.uniform(10, 100, n_stocks * n_dates)
})
financials = pd.DataFrame({
'股票代码': [f'{i:06d}.SH' for i in range(n_stocks)],
'ROE': np.random.uniform(5, 30, n_stocks),
'市值': np.random.uniform(50, 5000, n_stocks)
})
# 方法1:未优化
start_time = time.time()
result_slow = pd.merge(quotes, financials, on='股票代码')
slow_time = time.time() - start_time
# 方法2:将键值转为 category 类型
quotes_opt = quotes.copy()
quotes_opt['股票代码'] = quotes_opt['股票代码'].astype('category')
financials_opt = financials.copy()
financials_opt['股票代码'] = financials_opt['股票代码'].astype('category')
start_time = time.time()
result_fast = pd.merge(quotes_opt, financials_opt, on='股票代码')
fast_time = time.time() - start_time
print(f'未优化合并时间: {slow_time:.2f}秒')
print(f'优化后合并时间: {fast_time:.2f}秒')
print(f'性能提升: {slow_time/fast_time:.1f}倍')未优化合并时间: 0.86秒
优化后合并时间: 0.12秒
性能提升: 7.3倍
| 优化策略 | 说明 |
|---|---|
| 键值类型优化 | 将字符串键值转换为 category 类型 |
| 索引优化 | 对键值列建立索引(df.set_index()) |
| 避免重复 | 合并前检查并删除重复数据 |
| 分块处理 | 超大文件考虑分块读取和合并 |
| 使用 Dask | 超出内存容量时使用并行计算框架 |
# 行情数据
quotes = pd.DataFrame({
'股票代码': ['600519.SH', '600519.SH', '000858.SZ'],
'日期': ['2024-01-01', '2024-01-02', '2024-01-01'],
'收盘价': [1850.0, 1870.0, 158.5]
})
# 财务数据
financals = pd.DataFrame({
'股票代码': ['600519.SH', '600519.SH', '000858.SZ'],
'日期': ['2024-01-01', '2024-01-02', '2024-01-01'],
'PE': [35.2, 35.8, 25.8]
})
# 基于股票代码 + 日期两个键进行合并
merged = pd.merge(
quotes, financals,
on=['股票代码', '日期'], how='inner'
)
print('多键连接结果:')
print(merged)多键连接结果:
股票代码 日期 收盘价 PE
0 600519.SH 2024-01-01 1850.0 35.2
1 600519.SH 2024-01-02 1870.0 35.8
2 000858.SZ 2024-01-01 158.5 25.8
\[ R \bowtie_{k_1, k_2} S = \{ (r, s) \mid r[k_1] = s[k_1] \land r[k_2] = s[k_2] \} \]
只有所有指定的键值都匹配时,两行数据才会被连接。
应用场景:确保分析的是同一股票在同一天的完整数据,避免错误地将不同日期的数据拼接在一起。
pd.concat():堆叠数据,适合结构相同的数据框沿行/列方向拼接pd.merge():关联数据,基于键值列进行水平合并,支持四种连接类型df.join():索引合并,merge 的简化版,适合索引已设好的场景category 类型可显著提升大数据集合并速度[商业大数据分析与应用]